Angular 2+ and debounce
Asked Answered
N

16

207

In AngularJS I was able to debounce a model by using ng-model options.

ng-model-options="{ debounce: 1000 }"

How can I debounce a model in Angular?
I tried to search for debounce in the docs but I couldn't find anything.

https://angular.io/search/#stq=debounce&stp=1

A solution would be to write my own debounce function, for example:

import {Component, Template, bootstrap} from 'angular2/angular2';

// Annotation section
@Component({
  selector: 'my-app'
})
@Template({
  url: 'app.html'
})
// Component controller
class MyAppComponent {
  constructor() {
    this.firstName = 'Name';
  }
    
  changed($event, el){
    console.log("changes", this.name, el.value);
    this.name = el.value;
  }

  firstNameChanged($event, first){
    if (this.timeoutId) window.clearTimeout(this.timeoutID);
    this.timeoutID = window.setTimeout(() => {
        this.firstName = first.value;
    }, 250)
  }
    
}
bootstrap(MyAppComponent);

And my html

<input type=text [value]="firstName" #first (keyup)="firstNameChanged($event, first)">

But I'm looking for a built in function, is there one in Angular?

Numbles answered 17/8, 2015 at 13:9 Comment(1)
This might be relevant github.com/angular/angular/issues/1773, not implented yet apparently.Jojo
Y
236

Updated for RC.5

With Angular 2 we can debounce using RxJS operator debounceTime() on a form control's valueChanges observable:

import {Component}   from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable}  from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input type=text [value]="firstName" [formControl]="firstNameControl">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName        = 'Name';
  firstNameControl = new FormControl();
  formCtrlSub: Subscription;
  resizeSub:   Subscription;
  ngOnInit() {
    // debounce keystroke events
    this.formCtrlSub = this.firstNameControl.valueChanges
      .debounceTime(1000)
      .subscribe(newValue => this.firstName = newValue);
    // throttle resize events
    this.resizeSub = Observable.fromEvent(window, 'resize')
      .throttleTime(200)
      .subscribe(e => {
        console.log('resize event', e);
        this.firstName += '*';  // change something to show it worked
      });
  }
  ngDoCheck() { console.log('change detection'); }
  ngOnDestroy() {
    this.formCtrlSub.unsubscribe();
    this.resizeSub  .unsubscribe();
  }
} 

Plunker

The code above also includes an example of how to throttle window resize events, as asked by @albanx in a comment below.


Although the above code is probably the Angular-way of doing it, it is not efficient. Every keystroke and every resize event, even though they are debounced and throttled, results in change detection running. In other words, debouncing and throttling do not affect how often change detection runs. (I found a GitHub comment by Tobias Bosch that confirms this.) You can see this when you run the plunker and you see how many times ngDoCheck() is being called when you type into the input box or resize the window. (Use the blue "x" button to run the plunker in a separate window to see the resize events.)

A more efficient technique is to create RxJS Observables yourself from the events, outside of Angular's "zone". This way, change detection is not called each time an event fires. Then, in your subscribe callback methods, manually trigger change detection – i.e., you control when change detection is called:

import {Component, NgZone, ChangeDetectorRef, ApplicationRef, 
        ViewChild, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input #input type=text [value]="firstName">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName = 'Name';
  keyupSub:  Subscription;
  resizeSub: Subscription;
  @ViewChild('input') inputElRef: ElementRef;
  constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef,
    private appref: ApplicationRef) {}
  ngAfterViewInit() {
    this.ngzone.runOutsideAngular( () => {
      this.keyupSub = Observable.fromEvent(this.inputElRef.nativeElement, 'keyup')
        .debounceTime(1000)
        .subscribe(keyboardEvent => {
          this.firstName = keyboardEvent.target.value;
          this.cdref.detectChanges();
        });
      this.resizeSub = Observable.fromEvent(window, 'resize')
        .throttleTime(200)
        .subscribe(e => {
          console.log('resize event', e);
          this.firstName += '*';  // change something to show it worked
          this.cdref.detectChanges();
        });
    });
  }
  ngDoCheck() { console.log('cd'); }
  ngOnDestroy() {
    this.keyupSub .unsubscribe();
    this.resizeSub.unsubscribe();
  }
} 

Plunker

I use ngAfterViewInit() instead of ngOnInit() to ensure that inputElRef is defined.

detectChanges() will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick() instead. (I put a call to ApplicationRef.tick() in comments in the plunker.) Note that calling tick() will cause ngDoCheck() to be called.

Yodel answered 25/4, 2016 at 19:24 Comment(7)
@Mark Rajcok I think instead of [value] , you should use [ngModel] , because [value] doesn't update the input value .Hexyl
is there any generic debounce method (for example to apply on window resize event)?Tuberculous
I would add this two lines after .debounceTime(1000) just to improve that (later in the subscribe you'll already have the value): .map((event: any) => event.target.value) .distinctUntilChanged()Gheber
@MarkRajcok I believe the CD issue you described in your answer is resolved by github.com/angular/zone.js/pull/843Pianette
@MarkRajcok I used this.ngzone.run(() => {}); to render response from subscribe method of a httpRequest invoked from this.ngzone.runOutsideAngular since this.cdref.detectChanges() was not aware of returned results. Don't know is it the correct way of doing this!Subservience
When would we need to unsubscribe to prevent memory leaks?Polychromy
@Polychromy Yes, acccording to netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3, we should unsubscribe from .fromEvent() subscriptionsRequiem
D
201

If you don't want to deal with @angular/forms, you can just use an RxJS Subject with change bindings.

view.component.html

<input [ngModel]='model' (ngModelChange)='changed($event)' />

view.component.ts

import { Subject } from 'rxjs';
import { Component }   from '@angular/core';
import 'rxjs/add/operator/debounceTime';

export class ViewComponent {
    model: string;
    modelChanged: Subject<string> = new Subject<string>();

    constructor() {
        this.modelChanged
            .debounceTime(300) // wait 300ms after the last event before emitting last event
            .distinctUntilChanged() // only emit if value is different from previous value
            .subscribe(model => this.model = model);
    }

    changed(text: string) {
        this.modelChanged.next(text);
    }
}

This does trigger change detection. For a way that doesn't trigger change detection, check out Mark's answer.


Update

.pipe(debounceTime(300), distinctUntilChanged()) is needed for rxjs 6.

Example:

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }
Dietetic answered 24/11, 2016 at 3:32 Comment(10)
I prefer this solution! Worked with angular 2.0.0, rxjs 5.0.0-beta 12Interlocution
Worked perfectly, simple and clear, no form involved. I'm on Angular 4.1.3, rxjs 5.1.1Multivocal
I think this is superior solution since it has the option to work with forms if needed, but removes that dependency making implementation that much simpler. Thanks.Deathbed
.pipe(debounceTime(300), distinctUntilChanged()) is needed for rxjs 6Shaitan
The solution saved me. I was using keyUp event on input.nativeElement in a mat-table, that stopped working when the number of columns was changedRexferd
Is there anyway to prevent change detection with this method?Shotputter
Certainly it is possible to prevent change detection with this method, as I just recently adapted both this answer and the selected one, and worked just fine. I even added an extra conditional check to maximize api rest call efficiency. Working on ng v. 7.xEmmie
distinctUntilChanged() is a life saver, otherwise your form values will refresh all the time and interrupt i.e. sending queryParams!Lilalilac
import { Subject } from 'rxjs/Subject'; gives error with angular 8. Use import { Subject } from 'rxjs';Corker
Do you think we need to do unsubscribe or another thing on OnDestroy?Preterite
D
67

Since the topic is old, most of the answers don't work on Angular 6-13 and/or use other libs.
So here is a short and simple solution for Angular 6+ with RxJS.

Import the necessary stuff first:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

Implement the ngOnInit and ngOnDestroy:

export class MyComponent implements OnInit, OnDestroy {
  public notesText: string;
  public notesModelChanged: Subject<string> = new Subject<string>();
  private notesModelChangeSubscription: Subscription

  constructor() { }

  ngOnInit() {
    this.notesModelChangeSubscription = this.notesModelChanged
      .pipe(
        debounceTime(2000),
        distinctUntilChanged()
      )
      .subscribe(newText => {
        this.notesText = newText;
        console.log(newText);
      });
  }

  ngOnDestroy() {
    this.notesModelChangeSubscription.unsubscribe();
  }
}

Use this way:

<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' />

P.S. For more complex and efficient solutions you might still want to check other answers.

Dodeca answered 9/9, 2018 at 12:7 Comment(6)
@JustShadow Thank you! It was really helpful.Fredrick
This works perfect on the first try. But when I delete the searched text somehow the next request takes too long to respond.Dustproof
That's strange. It still works fine on my side. Could you please share more info or maybe open a new question for that?Dodeca
hats off... thank you @JustShadow! @SadikshaGautam must have already got the solution by now but for new ones... you might need to just lower the debouceTime(2000) from 2000 milliseconds to something lower, perhaps the usual delay which is 300milliseconds.Mclean
In Angular14 I'm getting: error TS2345: Argument of type 'Event' is not assignable to parameter of type 'string'.Chalkstone
I found the issue - you have to have: import { FormsModule } from '@angular/forms'; and also put FormsModule in the imports: attribute of your @NgModule that needs to use ngModel. You also must have a "name" attribute on your <input> element.Chalkstone
B
37

It could be implemented as Directive

import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { NgControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { Subscription } from 'rxjs';

@Directive({
  selector: '[ngModel][onDebounce]',
})
export class DebounceDirective implements OnInit, OnDestroy {
  @Output()
  public onDebounce = new EventEmitter<any>();

  @Input('debounce')
  public debounceTime: number = 300;

  private isFirstChange: boolean = true;
  private subscription: Subscription;

  constructor(public model: NgControl) {
  }

  ngOnInit() {
    this.subscription =
      this.model.valueChanges
        .debounceTime(this.debounceTime)
        .distinctUntilChanged()
        .subscribe(modelValue => {
          if (this.isFirstChange) {
            this.isFirstChange = false;
          } else {
            this.onDebounce.emit(modelValue);
          }
        });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

use it like

<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">

component sample

import { Component } from "@angular/core";

@Component({
  selector: 'app-sample',
  template: `
<input[(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
<input[(ngModel)]="value" (onDebounce)="asyncDoSomethingWhenModelIsChanged($event)">
`
})
export class SampleComponent {
  value: string;

  doSomethingWhenModelIsChanged(value: string): void {
    console.log({ value });
  }

  async asyncDoSomethingWhenModelIsChanged(value: string): Promise<void> {
    return new Promise<void>(resolve => {
      setTimeout(() => {
        console.log('async', { value });
        resolve();
      }, 1000);
    });
  }
} 
Brierwood answered 10/2, 2017 at 12:37 Comment(11)
with more imports, that worked for me: import "rxjs/add/operator/debounceTime"; import "rxjs/add/operator/distinctUntilChanged";Revalue
This by far makes it the simplest to implement application wideNalley
Isn't working checkout: stackblitz.com/edit/ng2-debounce-example?file=src/app/…Silsby
@Shyamal Parikh, it is working. You need to add (onDebounce)="doSomethingWhenModelIsChanged()" and the implementation to the componentBrierwood
isFirstChange is used not to emit on initializeBrierwood
A logger shows me my event this.debounceEvent.emit(modelValue); is emitted, but my search(searchTerm: string): void method is not called. Why do we need the [(ngModel)]="searchTerm" in <input matInput [(ngModel)]="searchTerm" (appOnDebounce)="search($event)" placeholder="Search..." autocomplete="off">?Ethiop
I posted my question #53648888Ethiop
It would be nice if you can update your answer and use the new pipe syntaxDouro
Works in Angular 8 and rxjs 6.5.2 with the following changes. If you want to use the pipe syntax, change the following: import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; to import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; and this.model.valueChanges .debounceTime(this.debounceTime) .distinctUntilChanged() to this.model.valueChanges .pipe( debounceTime(this.debounceTime), distinctUntilChanged() )Whitehot
Works in Angular 9 and rxjs 6.5.4 with changes @Whitehot stated in his comment. Just don't forget to export the directive in the module where you're creating it. And don't forget to include the module you're creating this directive in, into the module where you are using it.Tambourine
Also, a minor improvement. Instead of doing the isFirstChange check, you can just add a filter filter(value => value != null) as first in the pipe(). Another one would be to not do a search for less than, say, 2 characters. That would be another filter like this: filter(value => value.length > 2). Imports: import { debounceTime, distinctUntilChanged, map, filter } from 'rxjs/operators';.Tambourine
S
31

Not directly accessible like in angular1 but you can easily play with NgFormControl and RxJS observables:

<input type="text" [ngFormControl]="term"/>

this.items = this.term.valueChanges
  .debounceTime(400)
  .distinctUntilChanged()
  .switchMap(term => this.wikipediaService.search(term));

This blog post explains it clearly: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Here it is for an autocomplete but it works all scenarios.

Seismography answered 7/1, 2016 at 13:40 Comment(2)
but there is an error from service, this is not running againNiggerhead
I dont understand the example. [...] is one-way target binding. Why can the container be notified of valueChanges? shouldn't it have to be sth. like (ngFormControl)="..."?Snaffle
A
27

You can create an RxJS (v.6) Observable that does whatever you like.

view.component.html

<input type="text" (input)="onSearchChange($event.target.value)" />

view.component.ts

import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class ViewComponent {
    searchChangeObserver;

  onSearchChange(searchValue: string) {

    if (!this.searchChangeObserver) {
      new Observable(observer => {
        this.searchChangeObserver = observer;
      }).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
        .pipe(distinctUntilChanged()) // only emit if value is different from previous value
        .subscribe(console.log);
    }

    this.searchChangeObserver.next(searchValue);
  }  


}
Absorbent answered 16/5, 2017 at 5:11 Comment(4)
Thanks that helped, however I think the import should be from rsjs/Rx, I had errors when using the import the way you wrote it... so in my case it's now: import { Observable } from 'rxjs/Rx';Tacnaarica
@Tacnaarica It depends on the rxjs version. In version 6 it is: import { Observable } from 'rxjs';.Absorbent
Thanks! As an aside, you can just use one pipe call pipe(debounceTime(300), distinctUntilChanged())Monocot
searchChangeObserver is a Subscriber, so searchChangeSubscriber will be a better name.Cinchonine
G
16

For anyone using lodash, it is extremely easy to debounce any function:

changed = _.debounce(function() {
    console.log("name changed!");
}, 400);

then just throw something like this into your template:

<(input)="changed($event.target.value)" />
Gearing answered 16/3, 2017 at 22:27 Comment(3)
or just (input)="changed($event.target.value)"Panhandle
Thank you for answering with lodash :)Floris
I believe this will still trigger the Angular change detection on every single change, regardless of the debouncing.Auld
T
8

Solution with initialization subscriber directly in event function:

import {Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';

class MyAppComponent {
    searchTermChanged: Subject<string> = new Subject<string>();

    constructor() {
    }

    onFind(event: any) {
        if (this.searchTermChanged.observers.length === 0) {
            this.searchTermChanged.pipe(debounceTime(1000), distinctUntilChanged())
                .subscribe(term => {
                    // your code here
                    console.log(term);
                });
        }
        this.searchTermChanged.next(event);
    }
}

And html:

<input type="text" (input)="onFind($event.target.value)">
Thrall answered 21/11, 2018 at 9:56 Comment(2)
Works totally fine for angular 8 prime ng autocomplete textbox. Thanks a lot.Entropy
awesome answer ..keep it upCeratoid
E
4

I solved this by writing a debounce decorator. The problem described could be solved by applying the @debounceAccessor to the property's set accessor.

I've also supplied an additional debounce decorator for methods, which can be useful for other occasions.

This makes it very easy to debounce a property or a method. The parameter is the number of milliseconds the debounce should last, 100 ms in the example below.

@debounceAccessor(100)
set myProperty(value) {
  this._myProperty = value;
}


@debounceMethod(100)
myMethod (a, b, c) {
  let d = a + b + c;
  return d;
}

And here's the code for the decorators:

function debounceMethod(ms: number, applyAfterDebounceDelay = false) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        if (applyAfterDebounceDelay) {
          originalMethod.apply(this, args);
        }
        timeoutId = null;
      }, ms);

      if (!applyAfterDebounceDelay) {
        return originalMethod.apply(this, args);
      }
    }
  }
}

function debounceAccessor (ms: number) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalSetter = descriptor.set;
    descriptor.set = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        timeoutId = null;
      }, ms);
      return originalSetter.apply(this, args);
    }
  }
}

I added an additional parameter for the method decorator which let's you trigger the method AFTER the debounce delay. I did that so I could for instance use it when coupled with mouseover or resize events, where I wanted the capturing to occur at the end of the event stream. In this case however, the method won't return a value.

Enugu answered 16/2, 2017 at 16:5 Comment(0)
H
4

We can create a [debounce] directive which overwrites ngModel's default viewToModelUpdate function with an empty one.

Directive Code

@Directive({ selector: '[debounce]' })
export class MyDebounce implements OnInit {
    @Input() delay: number = 300;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit(): void {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.delay);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

How to use it

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>
Howze answered 15/12, 2017 at 0:1 Comment(1)
What's the namespace of Observable? Mine is coming without the "fromEvent" methodHelot
J
3

HTML file:

<input [ngModel]="filterValue"
       (ngModelChange)="filterValue = $event ; search($event)"
        placeholder="Search..."/>

TS file:

timer = null;
time = 250;
  search(searchStr : string) : void {
    clearTimeout(this.timer);
    this.timer = setTimeout(()=>{
      console.log(searchStr);
    }, time)
  }
Johiah answered 11/12, 2018 at 14:29 Comment(0)
B
3

DebounceTime in Angular 7 with RxJS v6

Source Link

Demo Link

enter image description here

In HTML Template

<input type="text" #movieSearchInput class="form-control"
            placeholder="Type any movie name" [(ngModel)]="searchTermModel" />

In component

    ....
    ....
    export class AppComponent implements OnInit {

    @ViewChild('movieSearchInput') movieSearchInput: ElementRef;
    apiResponse:any;
    isSearching:boolean;

        constructor(
        private httpClient: HttpClient
        ) {
        this.isSearching = false;
        this.apiResponse = [];
        }

    ngOnInit() {
        fromEvent(this.movieSearchInput.nativeElement, 'keyup').pipe(
        // get value
        map((event: any) => {
            return event.target.value;
        })
        // if character length greater then 2
        ,filter(res => res.length > 2)
        // Time in milliseconds between key events
        ,debounceTime(1000)        
        // If previous query is diffent from current   
        ,distinctUntilChanged()
        // subscription for response
        ).subscribe((text: string) => {
            this.isSearching = true;
            this.searchGetCall(text).subscribe((res)=>{
            console.log('res',res);
            this.isSearching = false;
            this.apiResponse = res;
            },(err)=>{
            this.isSearching = false;
            console.log('error',err);
            });
        });
    }

    searchGetCall(term: string) {
        if (term === '') {
        return of([]);
        }
        return this.httpClient.get('http://www.omdbapi.com/?s=' + term + '&apikey=' + APIKEY,{params: PARAMS.set('search', term)});
    }

    }
Buckeye answered 27/3, 2019 at 6:12 Comment(1)
Thank you for that amazing blog link!Ravi
D
3

You could also solve this by using a decorators, For an instance by using the debounce decorator from utils-decorator lib (npm install utils-decorators):

import {debounce} from 'utils-decorators';

class MyAppComponent {

  @debounce(500)
  firstNameChanged($event, first) {
   ...
  }
}
Disorder answered 12/4, 2020 at 20:41 Comment(1)
I like how clean this one looksRooster
L
2

Simple solution would be to create a directive which you can apply to any control.

import { Directive, ElementRef, Input, Renderer, HostListener, Output, EventEmitter } from '@angular/core';
import { NgControl } from '@angular/forms';

@Directive({
    selector: '[ngModel][debounce]',
})
export class Debounce 
{
    @Output() public onDebounce = new EventEmitter<any>();

    @Input('debounce') public debounceTime: number = 500;

    private modelValue = null;

    constructor(public model: NgControl, el: ElementRef, renderer: Renderer){
    }

    ngOnInit(){
        this.modelValue = this.model.value;

        if (!this.modelValue){
            var firstChangeSubs = this.model.valueChanges.subscribe(v =>{
                this.modelValue = v;
                firstChangeSubs.unsubscribe()
            });
        }

        this.model.valueChanges
            .debounceTime(this.debounceTime)
            .distinctUntilChanged()
            .subscribe(mv => {
                if (this.modelValue != mv){
                    this.modelValue = mv;
                    this.onDebounce.emit(mv);
                }
            });
    }
}

usage would be

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>
Lay answered 2/2, 2017 at 22:32 Comment(1)
This class is far from compiling in Angular 7.Ethiop
D
1

Spent hours on this, hopefully I can save someone else some time. To me the following approach to using debounce on a control is more intuitive and easier to understand for me. It's built on the angular.io docs solution for autocomplete but with the ability for me to intercept the calls without having to depend on tying the data to the DOM.

Plunker

A use case scenario for this might be checking a username after it's typed to see if someone has already taken it, then warning the user.

Note: don't forget, (blur)="function(something.value) might make more sense for you depending on your needs.

Dessalines answered 22/9, 2016 at 19:51 Comment(0)
S
0

This is the best solution I have found till now. Updates the ngModelon blur and debounce

import { Directive, Input, Output, EventEmitter,ElementRef } from '@angular/core';
import { NgControl, NgModel } from '@angular/forms';
import 'rxjs/add/operator/debounceTime'; 
import 'rxjs/add/operator/distinctUntilChanged';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';

@Directive({
    selector: '[ngModel][debounce]',
})
export class DebounceDirective {
    @Output()
    public onDebounce = new EventEmitter<any>();

    @Input('debounce')
    public debounceTime: number = 500;

    private isFirstChange: boolean = true;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit() {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.debounceTime);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

as borrowed from https://mcmap.net/q/49644/-angular-2-and-debounce

Then in HTML:

<input [(ngModel)]="hero.name" 
        [debounce]="3000" 
        (blur)="hero.name = $event.target.value"
        (ngModelChange)="onChange()"
        placeholder="name">

On blur the model is explicitly updated using plain javascript.

Example here: https://stackblitz.com/edit/ng2-debounce-working

Silsby answered 21/2, 2018 at 10:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.